﻿using System;
using System.Diagnostics;
using System.IO;
using System.Threading;

namespace TPOJ.Core
{
    /// <summary>
    /// The runner of the judge engine.
    /// It loads the program executable and runs it in a sandbox.
    /// </summary>
    class Runner
    {
        private Result result = new Result();
        private string commandLine;
        private bool isExitedEventHandled = false; //Used for threads synchronization.
        private bool isOutputDataHandled = false; //The caller waits for events being handled.

        public string InputPath { get; set; }
        public string OutputPath { get; set; }
        public string BinPath { get; set; }
        public int TimeLimit { get; set; }
        public int MemoryLimit { get; set; }
        public Language Language { get; set; }

        /// <summary>
        /// Create a runner with given params.
        /// </summary>
        /// <param name="inputPath">Input data file path</param>
        /// <param name="outputPath">Output data file path</param>
        /// <param name="binPath">Executable path</param>
        /// <param name="timeLimit">Time limit in MS</param>
        /// <param name="memoryLimit">Memory limit in KB</param>
        /// <param name="language">The programming language used</param>
        public Runner(string inputPath, string outputPath, string binPath, int timeLimit, int memoryLimit, Language language)
        {
            this.InputPath = inputPath;
            this.OutputPath = outputPath;
            this.BinPath = binPath;
            this.TimeLimit = (timeLimit + language.TimeFix) * language.TimeFactor;
            this.MemoryLimit = memoryLimit + language.MemoryFix;
            this.Language = language;
            ParseCommandLine();
        }

        /// <summary>
        /// Parse the configuration to get the actual command line.
        /// </summary>
        private void ParseCommandLine()
        {
            this.commandLine = this.Language.RunCommandLine.Replace("%1", this.BinPath);
        }

        /// <summary>
        /// Load and run the program.
        /// </summary>
        /// <returns>The running result</returns>
        public Result Run()
        {
            result.ResultType = ResultType.RuntimeError;
            Sandbox sandbox = new Sandbox();

            ProcessStartInfo startInfo = new ProcessStartInfo(this.commandLine);
            startInfo.WindowStyle = ProcessWindowStyle.Hidden;
            startInfo.CreateNoWindow = true;
            startInfo.UseShellExecute = false;
            startInfo.RedirectStandardOutput = true;
            startInfo.RedirectStandardError = true;
            if (this.InputPath != null)
                startInfo.RedirectStandardInput = true;

            //Process.EnterDebugMode();
            Process p = sandbox.CreateProcess(startInfo);
            p.EnableRaisingEvents = true;
            p.Exited += p_Exited;
            p.OutputDataReceived += new DataReceivedEventHandler(p_OutputDataReceived);

            StreamWriter input = p.StandardInput;
            StreamReader reader = new StreamReader(this.InputPath);
            input.WriteLine(reader.ReadToEnd());

            p.BeginOutputReadLine();

            if (sandbox.WaitForDebugEvent(this.TimeLimit))
            {
                result.ExitCode = 0;
                result.ErrorMessage = null;
            }
            else
            {
                sandbox.Kill();
                if (string.IsNullOrEmpty(sandbox.ErrorMessage))
                {
                    result.ExitCode = -1;
                    result.ResultType = ResultType.TimeLimitExceeded;
                }
                else
                {
                    result.ExitCode = -1;
                    result.ErrorMessage = sandbox.ErrorMessage;
                }
            }
            //Process.LeaveDebugMode();           

            if (string.IsNullOrEmpty(result.ErrorMessage))
            {
                p.Kill();
                //Wait for process exit.
                while (!p.HasExited)
                    Thread.Sleep(100);
                //Wait for process exited event to be handled.
                while (!this.isExitedEventHandled)
                    Thread.Sleep(100);
                //Wait for process output data received event to be handled.
                while (!this.isOutputDataHandled)
                    Thread.Sleep(100);
            }
            else
            {
                p.Exited -= p_Exited;
                p.Kill();
            }
            p.Close();
            p.Dispose();

            FixResult();
            return result;
        }

        /// <summary>
        /// Fix the result to balance the difference between different runtime performance.
        /// </summary>
        private void FixResult()
        {
            result.Memory -= this.Language.MemoryFix;
            result.Time -= this.Language.TimeFix;
            result.Time /= this.Language.TimeFactor;
            result.Memory = (result.Memory > 0) ? result.Memory : 0;
            result.Time = (result.Time > 0) ? result.Time : 0;
        }

        /// <summary>
        /// User program process exited event handler.
        /// Tests for MemoryLimitExceeded.
        /// </summary>
        /// <param name="sender">User process</param>
        /// <param name="e">EventArgs</param>
        private void p_Exited(object sender, EventArgs e)
        {
            Process process = sender as Process;
            long usedMemory = process.PeakPagedMemorySize64;
            if (usedMemory / 1024 > this.MemoryLimit)
            {
                if (string.IsNullOrEmpty(result.ErrorMessage))
                {
                    result.ExitCode = -3;
                    result.ResultType = ResultType.MemoryLimitExceeded;
                }
            }
            result.Time = process.TotalProcessorTime.TotalMilliseconds;
            result.Memory = usedMemory / 1024;
            isExitedEventHandled = true;
        }

        /// <summary>
        /// User program process output data received event handler.
        /// Write output data to output file for comparing.
        /// </summary>
        /// <param name="sender">User process</param>
        /// <param name="e">EventArgs</param>
        private void p_OutputDataReceived(object sender, DataReceivedEventArgs e)
        {
            using (StreamWriter writer = File.AppendText(this.OutputPath))
            {
                if (e.Data != null)
                    writer.WriteLine(e.Data);
            }
            isOutputDataHandled = true;
        }
    }
}
